using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Win32;

namespace DoxyComment
{
	internal interface IProviderInfo
	{
		string DisplayName
		{
			get;
		}

		ICommentProvider CreateProviderInstance();

		void SaveProviderProperties(ICommentProvider provider);
	}

	/// <summary>
	/// The ConfigHelper class encapsulates the persistent configuration for
	/// DoxyComment. Currently the configuration is saved in the the registry.
	/// Configuration is a singleton object accessibly through the
	/// <see cref="Instance"/> property.
	/// </summary>
	internal class ConfigHelper
	{
		// The one and only instance of this class.
		private static ConfigHelper instance;

		// The root key for standard options in registry.
		private static string optionsRootKey = "Software\\SourceForge.net\\DoxyComment";

		private class ProviderInfo : IProviderInfo
		{
			private string displayName;
			private Type providerType;

			// Root key for provider properties in the registry. Each provider type
			// will load and save properties to and from a sub-key of this key.
			private static string providerRootKey = "Software\\SourceForge.net\\DoxyComment\\Provider options";

			public ProviderInfo(Type providerType)
			{
				if (!CheckType(providerType))
				{
					throw new ArgumentException("Illegal provider type supplied");
				}

				this.providerType = providerType;

				// Get the display name for this provider type
				CommentProviderFriendlyNameAttribute[] attrs = (CommentProviderFriendlyNameAttribute[])providerType.GetCustomAttributes(typeof(CommentProviderFriendlyNameAttribute), true);
				if (attrs.Length > 0)
				{
					// Display name can be specified with the CommentProviderFriendlyName
					// attribute
					displayName = attrs[0].FriendlyName;
				}
				else
				{
					displayName = providerType.Name;
				}
			}

			/// <summary>
			/// Function checks if a given type fulfills the requirements to be
			/// a comment provider.
			/// </summary>
			/// <param name="checkType">The type to check</param>
			/// <returns>True if the type is a valid comment provider type, otherwise false.</returns>
			public static bool CheckType(Type checkType)
			{
				// Is current type class?
				if (!checkType.IsClass)
				{
					// Comment providers must be class types
					return false;
				}

				// Check if the current type supports the ICommentProvider
				// interface (Type objects share reference equality).
				if (checkType.GetInterface(typeof(ICommentProvider).FullName) == typeof(ICommentProvider))
				{
					// Check for a default constructor
					ConstructorInfo constructor = checkType.GetConstructor(new Type[] { });
					if (constructor != null)
					{
						// This is a valid custom provider type
						return true;
					}
				}

				return false;
			}

			public Type ProviderType
			{
				get
				{
					return providerType;
				}
			}

			#region IProviderInfo Members

			public string DisplayName
			{
				get
				{
					return displayName;
				}
			}

			public ICommentProvider CreateProviderInstance()
			{
				// Construct provider object
				ConstructorInfo providerConstructor = providerType.GetConstructor(new Type[] { });
				ICommentProvider provider = (ICommentProvider)providerConstructor.Invoke(null);

				LoadProviderProperties(provider);

				return provider;
			}

			public void SaveProviderProperties(ICommentProvider provider)
			{
				if (provider.GetType() != providerType)
				{
					throw new ArgumentException("Illegal provider!!");
				}

				// Persist all fields marked with the CommentProviderProperty
				// attribute to registry
				string providerKey = providerRootKey + "\\" + provider.GetType().FullName;
				BindingFlags fieldTypes = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;

				foreach (FieldInfo fi in provider.GetType().GetFields(fieldTypes))
				{
					if (fi.GetCustomAttributes(typeof(CommentProviderPropertyAttribute), true).Length > 0)
					{
						RegistryHelper.SetValue(providerKey, fi.Name, fi.GetValue(provider));
					}
				}
			}
			
			#endregion

			private void LoadProviderProperties(ICommentProvider provider)
			{
				// Load provider properties from registry
				string providerKey = providerRootKey + "\\" + provider.GetType().FullName;
				BindingFlags fieldTypes = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;

				foreach (FieldInfo fi in provider.GetType().GetFields(fieldTypes))
				{
					if (fi.GetCustomAttributes(typeof(CommentProviderPropertyAttribute), true).Length > 0)
					{
						object defValue = fi.GetValue(provider);
						// TODO: verify defValue type to be registry compliant!
						object val = RegistryHelper.GetValue(providerKey, fi.Name, defValue);
						if (val != defValue)
						{
							fi.SetValue(provider, val);
						}
					}
				}
			}

			#region Object Members

			/// <summary>
			/// ProviderInfo objects are bound to a unique <see cref="Type"/>
			/// object.
			/// </summary>
			/// <returns></returns>
			public override int GetHashCode()
			{
				return providerType.GetHashCode();
			}

			#endregion
		}

		// Dictionary holds all provider types.
		private List<ProviderInfo> providerTypes;

		/// <summary>
		/// Private constructor to prevent user creation.
		/// </summary>
		private ConfigHelper()
		{
			providerTypes = new List<ProviderInfo>();

			// Add default comment provider type
			providerTypes.Add(new ProviderInfo(typeof(DefaultCommentProvider)));

			// Scan for custom comment providers
			AddCustomProviders();
		}

		/// <summary>
		/// Function scans for custom comment providers. Custom provider
		/// assemblies must be placed in a sub-directory of the main DoxyComment
		/// assembly called "Custom Providers".
		/// </summary>
		private void AddCustomProviders()
		{
			string installPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			if (installPath.Length == 0)
			{
				// If for some strange reason the use install in the root of a
				// drive
				installPath = Path.GetPathRoot(Assembly.GetExecutingAssembly().Location);
			}

			string providerPath = Path.Combine(installPath,"Custom Providers");
			if (Directory.Exists(providerPath))
			{
				string[] assemblyFiles = Directory.GetFiles(providerPath, "*.dll");
				foreach (string assemblyFile in assemblyFiles)
				{
					try
					{
						// Try to load the custom provider assembly
						Assembly asm = Assembly.LoadFrom(assemblyFile);

						// Run through all classes in assembly
						foreach (Type curType in asm.GetTypes())
						{
							if (ProviderInfo.CheckType(curType))
							{
								providerTypes.Add(new ProviderInfo(curType));
							}
						}
					}
					catch (Exception)
					{
						// TODO: logging of error
					}
				}
			}
		}

		/// <summary>
		/// Property returns the one and only instance of the configuration
		/// class.
		/// </summary>
		public static ConfigHelper Instance
		{
			get
			{
				if (instance == null)
				{
					instance = new ConfigHelper();
				}
				return instance;
			}
		}

		/// <summary>
		/// Property determines if DoxyComment should indent comment blocks
		/// horizontally when inserting.
		/// </summary>
		public bool IndentCommentBlocks
		{
			get
			{
				return (int)RegistryHelper.GetValue(optionsRootKey,"IndentCommentBlocks",1) != 0 ? true:false;;
			}
			set
			{
				RegistryHelper.SetValue(optionsRootKey, "IndentCommentBlocks", value ? 1 : 0);
			}
		}

		/// <summary>
		/// Property determines if DoxyComment should try to insert comment
		/// blocks at the definition
		/// </summary>
		public bool InsertAtDefinition
		{
			get
			{
				return (int)RegistryHelper.GetValue(optionsRootKey, "InsertAtDefinition", 1) != 0 ? true : false; ;
			}
			set
			{
				RegistryHelper.SetValue(optionsRootKey, "InsertAtDefinition", value ? 1 : 0);
			}
		}

		/// <summary>
		/// Property returns an array of all valid comment providers types.
		/// </summary>
		public IProviderInfo[] ProviderTypes
		{
			get
			{
				return providerTypes.ToArray();
			}
		}

		/// <summary>
		/// Property holds the currently active comment provider type.
		/// </summary>
		public IProviderInfo ActiveProviderType
		{
			get
			{
				// Get display name of active comment provider type from
				// registry- set default provider if no registry information is
				// found.
				string activeProviderClass = (string)RegistryHelper.GetValue(optionsRootKey,
					"ActiveProvider",providerTypes[0].ProviderType.FullName);

				// Search for active comment provider type in known provider types
				foreach (ProviderInfo pi in providerTypes)
				{
					if (pi.ProviderType.FullName == activeProviderClass)
					{
						// Found matching class name.
						return pi;
					}
				}

				// Active comment provider type was not found - return default
				// type (this should only happen when the user has deleted/
				// uninstalled an active custom provider).
				return providerTypes[0];
			}

			set
			{
				if ((value.GetType() != typeof(ProviderInfo)) ||
					!providerTypes.Contains((ProviderInfo)value))
				{
					throw new ArgumentException("Illegal provider type supplied!!!");
				}
				RegistryHelper.SetValue(optionsRootKey, "ActiveProvider", ((ProviderInfo)value).ProviderType.FullName);
			}
		}
	}

	/// <summary>
	/// Class is used to access registry for persistence of options. All
	/// registry access is done under the HKEY_CURRENT_USER root key.
	/// </summary>
	internal class RegistryHelper
	{
		/// <summary>
		/// Function fetches a value from the key. If the value is not present
		/// it will be written to the key with a default Value.
		/// </summary>
		/// <param name="keyName">The name of the key to fetch from.</param>
		/// <param name="valueName">The name of the value to fetch.</param>
		/// <param name="defValue">
		/// Default value for value. Must be a compatible registry type.
		/// </param>
		/// <returns>
		/// Function returns the value from the registry or the default value if
		/// no value exist in the registry.
		/// </returns>
		public static object GetValue(string keyName, string valueName,
			object defValue)
		{
			RegistryKey key = Registry.CurrentUser.CreateSubKey(keyName);

			// Query value
			object rv = key.GetValue(valueName);
			if ((rv == null) || (rv.GetType() != defValue.GetType()))
			{
				// Value not found or wrong type -> write default to
				// registry
				key.SetValue(valueName, defValue);
				rv = defValue;
			}

			key.Close();
			return rv;
		}

		/// <summary>
		/// Function writes a value to the registry.
		/// </summary>
		/// <param name="keyName"></param>
		/// <param name="valueName"></param>
		/// <param name="setValue"></param>
		public static void SetValue(string keyName, string valueName,
			object setValue)
		{
			RegistryKey key = Registry.CurrentUser.CreateSubKey(keyName);
			key.SetValue(valueName, setValue);
			key.Close();
		}
	}
}
